feat(sponsors): embeddable /sponsors.svg + /sponsors.png image router for READMEs#483
Conversation
…g, reset yoga init on failure Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…outer # Conflicts: # package.json
…fer casts, Response BodyInit) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 097b993c02
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…on limit) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The /sponsors.png and /sponsors.svg routes now serve the cron/webhook -warmed blob straight from R2 via the KV manifest (readImage). On a cold cache miss they render THIS request live and kick off a background refreshSponsorsCache(force) that warms all 4 blobs (svg/png x light/dark) for next time. Cache-Control widened to `public, s-maxage=600, max-age=600, stale-while-revalidate=86400`. The three landing loaders (en/cn/pt-BR) read getCachedSponsors(KV) — the cron/webhook-maintained snapshot when present, else the live GitHub fetch so the wall still works before the first refresh. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…KV parse + live cold render
- refresh: guard against clobbering a good snapshot with an empty (degraded)
fetch; read manifest once and reuse for the version check. Cold cache still
writes empty (nothing better to serve).
- store: writeSnapshot re-reads the manifest and only deletes the previous
version's R2 blobs if we are still the published version (concurrent-supersede
guard). readManifest/readData parse defensively -> null on corrupt JSON so
callers fall back instead of 500ing.
- routes/sponsors.{png,svg}: cold miss renders from the live loader (spec), not
KV-first getCachedSponsors; drop the now-unused import.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
crypto.subtle.verify's BufferSource param rejects the widened Uint8Array<ArrayBufferLike> under TS 5.7+; the fresh allocation is already backed by a plain ArrayBuffer, so annotate it. Type-only, no runtime change. Clears the last non-pre-existing tsc --noEmit error. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The image cold path already loads good sponsor data (loadSponsors()) and
renders the response from it. The background warm re-fetched via
loadSponsors({bypassCache:true}); if that 2nd fetch degraded to empty
while the 1st succeeded, refreshSponsorsCache — on a cold cache with no
manifest, so the isEmpty guard can't trip — published an empty first
snapshot over a request that had live data. Reuse the already-loaded
`sponsors` for the warm (also drops a redundant GitHub fetch per cold
miss). cron/webhook keep the live bypassed fetch (no preloaded payload).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…OCTOU) The isEmpty guard only skipped when a manifest already existed (`&& current`), and current was read ONCE before the slow render. On a cold cache an empty degraded refresh saw no manifest, rendered, and by writeSnapshot time a concurrent good refresh may have published — the empty writer then overwrote it (and its delete-guard removed the good version's R2 blobs). Drop the `&& current`: refuse to publish an all-empty (degraded) snapshot at all. A cold cache stays cold so cold-misses live-render and self-heal when GitHub recovers; the empty writer never reaches writeSnapshot, so it can't clobber a concurrently-published good snapshot. (Healthy fetch always has Special Thanks, so all-empty reliably means degraded.) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 8ead569f4d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ollback Two non-empty refreshes with different versions can overlap: the older reads the manifest at start, spends seconds rendering, and its writeSnapshot then wrote stale data+manifest back (and the delete-guard, seeing itself as current, deleted the newer version's R2 blobs). writeSnapshot now takes the version observed at refresh start and re-reads the pointer right before flipping: if a different version was published mid-render it aborts, dropping the blobs it just uploaded, so a slower/staler writer can't roll the cache back. Deletes now target the version we actually CAS'd against. Addresses Codex PR review P2. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5fce43e967
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…read errors Addresses two Codex PR review P2s: - store: writeSnapshot staged every render under a per-render id (sponsors/<version>/<renderId>/…) so a forced same-version re-render (daily avatar refresh) no longer overwrites the keys the live manifest serves. A partial r2.put failure or an aborted CAS can't corrupt or roll back the live cache — readers only ever follow the manifest to a complete folder, published by the single atomic flip. Cleanup now deletes the previous manifest's blobs by key (so a same-version render still frees the old folder). - store: readManifest/readData now wrap the whole read (not just JSON.parse) so a rejected kv.get (transient KV error) degrades to a miss (null) instead of 500ing the landing loader via getCachedSponsors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: caf3053f89
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
readImage wrapped only the manifest read; a rejected r2.get / arrayBuffer
(transient R2 error) escaped and 500'd /sponsors.{svg,png} before their cold
live-render fallback. Treat an R2 read failure as a cache miss (null) too, so a
broken warm cache degrades to a live render. Addresses Codex PR review P2.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
KV has no atomic compare-and-swap, so a ~ms read→put window in the manifest publish can't be closed at the KV layer. Document why it's acceptable here (overlap needs two refreshes within ms with changed data; self-heals next refresh) and that a Durable Object / R2 ETag CAS would be disproportionate for a daily-refreshed sponsor wall. Response to Codex PR review P2 (accepted). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a366f8676d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…ot bare live On a cold image miss the routes fetched live (loadSponsors) directly; a GitHub timeout/non-200/missing-token then rendered an empty wall that got served AND CDN-cached for s-maxage — even when a good sponsor snapshot sat in KV. Render from getCachedSponsors(KV) instead: last-good KV snapshot first, live only when KV has nothing; the background force-refresh still warms fresh blobs. Supersedes the earlier "cold miss -> live render" (spec doc updated). Addresses Codex PR review P2 (reverses the round-1 spec-compliance note on the same line). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: abc0615620
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ) | ||
| } | ||
| return new Response(body as BodyInit, { | ||
| headers: { 'Content-Type': contentType, 'Cache-Control': CACHE_CONTROL }, |
There was a problem hiding this comment.
Avoid caching degraded cold sponsor renders
When the image cache is cold (or a transient KV read makes getCachedSponsors() miss) and the live loader degrades to its all-empty sponsor list, this still returns the normal public 10-minute cache header. That lets GitHub/Camo or the edge cache an empty sponsor wall even though the refresh pipeline deliberately refuses to publish empty snapshots; use a no-store/very short TTL or an error response for all-empty cold renders. The PNG route has the same unconditional cold-response cache header.
Useful? React with 👍 / 👎.
What
Adds a runtime image router that renders the napi-rs sponsor wall as an embeddable image, so sponsors can be displayed in external READMEs (GitHub, npm, and the
napicrate):GET /sponsors.svgimage/svg+xmlGET /sponsors.pngimage/pngQuery param
?theme=light\|dark(defaultlight) so the image reads well on either README theme.How it works
Both wasm modules (
satori/yoga.wasm,@resvg/resvg-wasm/index_bg.wasm) are statically imported by the route files — Void's Cloudflare build turns them into realWebAssembly.Moduleobjects at the edge (verified in the workerd preview). Fonts (Manrope 700/500) are vendored topublic/fonts/and read at runtime via theASSETSbinding. Heavy logic lives in small, unit-testedlib/sponsors-image/*units; the route files are thin adapters.Only new dependency:
@resvg/resvg-wasm@2.6.2(the existing build-time@resvg/resvg-jsis untouched).Embedding in a README
Testing
Content-Type,Cache-Control: public, s-maxage=1800, max-age=600, stale-while-revalidate=86400, valid PNG magic bytes, and 17 live sponsors rendered.Notes
s-maxage+ the existing 10-min in-isolate sponsor cache; avatar fan-out only happens on a cache miss.🤖 Generated with Claude Code